//////////////////////////////////////////////////////////////////////////////////
//                                                                              //
//  This file is part of the buola project (https://code.google.com/p/buola/).  //
//                                                                              //
//  Copyright(c) 2007-2012 Xavi Gratal                                          //
//  gratal AT gmail DOT com                                                     //
//                                                                              //
//  Buola is free software: you can redistribute it and/or modify               //
//  it under the terms of the GNU General Public License as published by        //
//  the Free Software Foundation, either version 3 of the License, or           //
//  (at your option) any later version.                                         //
//                                                                              //
//  Buola is distributed in the hope that it will be useful,                    //
//  but WITHOUT ANY WARRANTY; without even the implied warranty of              //
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               //
//  GNU General Public License for more details.                                //
//                                                                              //
//  You should have received a copy of the GNU General Public License           //
//  along with buola.  If not, see <http://www.gnu.org/licenses/>.              //
//                                                                              //
//////////////////////////////////////////////////////////////////////////////////

#ifndef _BUOLA_DATETIME_CDATE_H_
#define _BUOLA_DATETIME_CDATE_H_

#include <buola/buola.h>
#include <buola/datetime/clocks.h>
#include <buola/datetime/ctimezone.h>
#include <buola/utility/operators.h>

namespace buola {

///\addtogroup datetime
///@{

DECLARE_NAME(FromAbsDayNumber);
    
////////////////////////////////////////////////////////////////////////////

/// date object

/// Represents a date in the gregorian calendar, broken into year, month
/// and day
///
/// It also contains enums for days of the week and months
///
////////////////////////////////////////////////////////////////////////////

class CDate : public ops::UComparison<CDate>
{
public:
    enum EMonth
    {
        JANUARY=1,
        FEBRUARY,
        MARCH,
        APRIL,
        MAY,
        JUNE,
        JULY,
        AUGUST,
        SEPTEMBER,
        OCTOBER,
        NOVEMBER,
        DECEMBER
    };

    enum EWeekDay
    {
        MONDAY=1,
        TUESDAY,
        WEDNESDAY,
        THURSDAY,
        FRIDAY,
        SATURDAY,
        SUNDAY
    };

    ///default constructor
    
    ///sets a null date
    CDate()
        :   mYear(-1)
        ,   mMonth(-1)
        ,   mDay(-1)
    {
    }

    ///copy constructor
    CDate(const CDate &pDate)
        :   mYear(pDate.mYear)
        ,   mMonth(pDate.mMonth)
        ,   mDay(pDate.mDay)
    {
    }

    ///construct from a point in calendar time

    ///\param pTime the point of time
    ///\param pTZ the timezone to use for the conversion. Defaults to local time.
    explicit CDate(const CCalendarTime &pTime,const CTimeZone& pTZ=CTimeZone());

    ///construct from \p pYear, \p pMonth and \p pDay

    ///does NOT check that the date is valid
    CDate(int pYear,int pMonth,int pDay)
        :   mYear(pYear)
        ,   mMonth(pMonth)
        ,   mDay(pDay)
    {
    }

    ///constructs the date from an absolute day number
    CDate(NFromAbsDayNumber,int pDayNumber)
    {
        int a=pDayNumber+32044;
        int b=(4*a+3)/146097;
        int c=a-((146097*b)/4);
        int d=(4*c+3)/1461;
        int e=c-((1461*d)/4);
        int m=(5*e+2)/153;
        mDay=e-((153*m+2)/5)+1;
        mMonth=m+3-12*(m/10);
        mYear=100*b+d-4800+m/10;
    }

    ///destructor
    ~CDate()
    {
    }

    ///set year, month and day of the date

    ///does NOT check that the date is valid
    void Set(int pYear,int pMonth,int pDay)
    {
        mYear=pYear;
        mMonth=pMonth;
        mDay=pDay;
    }

    ///returns \c true if the object has the special null value

    ///A null value is any value where year<0
    bool IsNull()
    {
        return mYear==-1;
    }

    ///returns true if the object represents a well-formed date
    bool IsWellFormed()
    {
        return (mYear>=1&&mMonth>=1&&mMonth<=12&&mDay>=1&&mDay<=LastDayOf(mYear,mMonth));
    }

    ///returns the date as text, according to current locale
    std::wstring GetText() const;
    ///returns the weekday name
    std::wstring GetWeekdayText() const;
    ///returns the short weekday name
    std::wstring GetShortWeekdayText() const;
    ///returns the month name
    std::wstring GetMonthText() const;
    ///returns the short month name
    std::wstring GetShortMonthText() const;
    ///returns the month and year as text, according to current locale
    std::wstring GetMonthYearText() const;
    ///returns an ISO string representing the date
    std::wstring GetISODate() const;

    ///returns the calendar time corresponding to the start of day
    CCalendarTime GetStartTime(const CTimeZone &pTZ=CTimeZone()) const;

    ///returns the calendar time corresponding to the end of day
    CCalendarTime GetEndTime(const CTimeZone &pTZ=CTimeZone()) const;

    ///returns the year as an integer
    int Year() const
    {
        return mYear;
    }

    ///returns the month as an integer in the range [1,12]
    int Month() const
    {
        return mMonth;
    }

    ///returns the day as an integer

    ///It returns 1 from the first day of the month
    int Day() const
    {
        return mDay;
    }

    ///returns the absolute day number corresponding to this date
    int AbsDayNumber() const
    {
        return AbsDayNumberOf(mYear,mMonth,mDay);
    }

    ///returns the weekday corresponding to this date

    ///monday=1, sunday=7
    int WeekDay() const
    {
        int a=(14-mMonth)/12;
        int y=mYear-a;
        int m=mMonth+12*a-2;
        return EWeekDay((mDay+y+y/4-y/100+y/400+(31*m)/12-1)%7+1);
    }

    ///returns the week number (within the year) corresponding to this date
    int WeekNumber() const
    {
        int lJulianBegin=AbsDayNumberOf(mYear,1,1);
        int lJulianDate=AbsDayNumber();
        int lDay=(lJulianBegin+3)%7;
        int lWeek=(lJulianDate+lDay-lJulianBegin+4)/7;

        if(lWeek<1) //recalculate using previous year
        {
            lJulianBegin=AbsDayNumberOf(mYear-1,1,1);
            lDay=(lJulianBegin+3)%7;
            lWeek=(lJulianDate+lDay-lJulianBegin+4)/7;
            return lWeek;
        }
        else if(lWeek>52)
        {
            if(lDay==6||(lDay==5&&IsLeapYear(mYear)))
                return lWeek;
            else
                return 1; //of next year
        }
        return lWeek;
    }

    ///equality operator
    bool operator==(const CDate &pRH) const
    {
        return mYear==pRH.mYear&&mMonth==pRH.mMonth&&
                mDay==pRH.mDay;
    }

    ///comparison less-than operator
    bool operator<(const CDate &pRH) const
    {
        return mYear<pRH.mYear||(mYear==pRH.mYear&&(mMonth<pRH.mMonth||(mMonth==pRH.mMonth&&mDay<pRH.mDay)));
    }

    ///assignment operator from calendar time
    CDate &operator=(const CCalendarTime &pTime)
    {
        *this=CDate(pTime);
        return *this;
    }

    ///returns a new \c CDate object representing the day after the one representing by this object
    CDate NextDay() const
    {
        if(mDay+1>LastDayOf(mYear,mMonth))
        {
            if(mMonth==DECEMBER)
            {
                return CDate(mYear+1,1,1);
            }
            else
            {
                return CDate(mYear,mMonth+1,1);
            }
        }
        return CDate(mYear,mMonth,mDay+1);
    }

    ///returns a new \c CDate object representing the date one month after the one representing by this object

    ///if the next month has less days than the current one, and the day would fall outside of the month,
    ///return the last day of the next month
    CDate NextMonth() const
    {
        if(mMonth==12)
        {
            return CDate(mYear+1,1,mDay);
        }
        else
        {
            int lLastDay=LastDayOf(mYear,mMonth+1);
            if(mDay>lLastDay)
            {
                return CDate(mYear,mMonth+1,lLastDay);
            }
            return CDate(mYear,mMonth+1,mDay);
        }
    }

    ///returns a new \c CDate object representing the date one year after the one representing by this object

    ///if the date represents a 29th of February, return 28th of February next year instead
    CDate NextYear() const
    {
        if(mMonth==2&&mDay==29)
        {
            return CDate(mYear+1,2,28);
        }
        return CDate(mYear+1,mMonth,mDay);
    }

    ///returns a new \c CDate object representing the day before the one representing by this object
    CDate PreviousDay() const
    {
        if(mDay==1)
        {
            if(mMonth==JANUARY)
            {
                return CDate(mYear-1,12,31);
            }
            else
            {
                return CDate(mYear,mMonth-1,LastDayOf(mYear,mMonth-1));
            }
        }
        return CDate(mYear,mMonth,mDay-1);
    }

    ///returns a new \c CDate object representing the date one month before the one representing by this object

    ///if the previous month has less days than the current one, and the day would fall outside of the month,
    ///return the last day of the previous month
    CDate PreviousMonth() const
    {
        if(mMonth==1)
        {
            return CDate(mYear-1,12,mDay);
        }
        else
        {
            int lLastDay=LastDayOf(mYear,mMonth-1);
            if(mDay>lLastDay)
            {
                return CDate(mYear,mMonth-1,lLastDay);
            }
            return CDate(mYear,mMonth-1,mDay);
        }
    }

    ///returns a new \c CDate object representing the date one year after the one representing by this object

    ///if the date represents a 29th of February, return 28th of February next year instead
    CDate PreviousYear() const
    {
        if(mMonth==2&&mDay==29)
        {
            return CDate(mYear-1,2,28);
        }
        return CDate(mYear-1,mMonth,mDay);
    }

    ///returns a new \c CDate object representing the first occurence of a \p pWD after the current day

    ///monday=1, sunday=7
    CDate NextWeekDay(int pWD)
    {
        return *this+((pWD-WeekDay()+7)%7);
    }

    ///returns a new \c CDate object representing the first occurence of a \p pWD before the current day

    ///monday=1, sunday=7
    CDate PreviousWeekDay(int pWD) const
    {
        return *this-((WeekDay()-pWD+7)%7);
    }

    ///returns a new \c CDate object, representing a date \p pDays after the current one
    CDate operator+(int pDays) const
    {
        return CDate(nFromAbsDayNumber,AbsDayNumberOf(mYear,mMonth,mDay)+pDays);
    }

    ///returns a new \c CDate object, representing a date \p pDays before the current one
    CDate operator-(int pDays) const
    {
        return CDate(nFromAbsDayNumber,AbsDayNumberOf(mYear,mMonth,mDay)-pDays);
    }

    ///returns the number of days between the current date and \p pRH
    int operator-(const CDate &pRH) const
    {
        return AbsDayNumberOf(mYear,mMonth,mDay)-AbsDayNumberOf(pRH.mYear,
                            pRH.mMonth,pRH.mDay);
    }

    ///increment operator (prefix version)

    ///After applying the increment operator, the object represents the day after the date it
    ///represented previously
    CDate &operator++()
    {
        if(mDay+1>LastDayOf(mYear,mMonth))
        {
            if(mMonth==DECEMBER)
            {
                mYear++;
                mMonth=1;
                mDay=1;
            }
            else
            {
                mMonth++;
                mDay=1;
            }
        }
        else
        {
            mDay++;
        }

        return *this;
    }

    ///increment operator (postfix version)

    ///After applying the increment operator, the object represents the day after the date it
    ///represented previously
    CDate operator++(int)
    {
        CDate lOld(*this);
        ++(*this);
        return lOld;
    }

    ///decrement operator (prefix version)

    ///After applying the increment operator, the object represents the day before the date it
    ///represented previously
    CDate &operator--()
    {
        if(mDay==1)
        {
            if(mMonth==JANUARY)
            {
                mYear--;
                mMonth=12;
                mDay=31;
            }
            else
            {
                mMonth--;
                mDay=LastDayOf(mYear,mMonth);
            }
        }
        else
        {
            mDay--;
        }

        return *this;
    }

    ///decrement operator (postfix version)

    ///After applying the increment operator, the object represents the day before the date it
    ///represented previously
    CDate operator--(int)
    {
        CDate lOld(*this);
        --(*this);
        return lOld;
    }

    ///returns true if year \p pYear is a leap year
    static bool IsLeapYear(int pYear)
    {
        return !(pYear%4)&&(pYear%100||!(pYear%400));
    }

    ///returns the last day of month \p pMonth in year \p pYear
    static int LastDayOf(int pYear,int pMonth)
    {
        switch(pMonth)
        {
        case 2:
            return IsLeapYear(pYear)?29:28;
        case 4:
        case 6:
        case 9:
        case 11:
            return 30;
        default:
            return 31;
        }
    }

    ///returns the absolute day number corresponding to the given date
    static int AbsDayNumberOf(int pYear,int pMonth,int pDay)
    {
        int a=(14-pMonth)/12;
        int y=pYear+4800-a;
        int m=pMonth+12*a-3;
        return pDay+((153*m+2)/5)+365*y+y/4-y/100+y/400-32045;
    }

    ///returns the number of weeks in the given month
    static int WeeksInMonth(int pYear,int pMonth)
    {
        int lFirst=CDate(pYear,pMonth,1).WeekDay();
        return ((lFirst+LastDayOf(pYear,pMonth)-1)+6)/7;
    }

private:
    int mYear,mMonth,mDay;
};

///@}

/*namespace buola*/ }

#endif
